package br.edu.ufcg.greengrid.model;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import br.edu.ufcg.greengrid.model.job.Job;
import br.edu.ufcg.greengrid.model.job.scheduler.DummyJobScheduler;
import br.edu.ufcg.greengrid.model.job.scheduler.JobScheduler;
import br.edu.ufcg.greengrid.model.machine.Machine;
import br.edu.ufcg.greengrid.model.machine.state.MachineState.MSEnum;
import br.edu.ufcg.greengrid.model.machine.state.factory.MachineStateFactory;

import com.mallardsoft.tuple.Pair;
import com.mallardsoft.tuple.Tuple;

public class Grid {

	//TODO The two attributes below should belong to the same Machine Factory (note: Machine Factory).
	private MachineStateFactory msf;
	
	private Map<String, Long> machineIdToOperationsPerSecond;
	
	private List<Machine> machines;
	
	private Set<Machine> idleMachines;//Cache for performance improvement
	
	private int numMachinesNotBeingUsedByUser;//Cache for performance improvement
	
	private int gridDemand;//Cache for performance improvement
	
	private List<Job> waitingJobs;
	
	private List<Job> allSubmittedJobs;
	
	public static Map<String, Long> DEFAULT_MACHINE_ID_TO_OPERATIONS_PER_SECOND = new FakeMap<String, Long>(1L);
	
	private JobScheduler scheduler;
	
	private long maxJobFailures;
	
	public static JobScheduler DEFAULT_JOB_SCHEDULER = new DummyJobScheduler();
	
	public static final long DEFAULT_MAX_JOB_FAILURES = Long.MAX_VALUE;
	
	private static class FakeMap<K, V> extends HashMap<K, V> {
		
		/**
		 * 
		 */
		private static final long serialVersionUID = 1L;
		
		private V defaultValue;
		
		public FakeMap(V defaultValue){
			this.defaultValue = defaultValue;
		}
		
		public V get(Object key) {
			
			return defaultValue;
		}
		
		public boolean containsKey(Object key) {
			
			return true;
		}
		
	}
	
	
	public Grid() {
		this(Machine.DEFAULT_MACHINE_STATE_FACTORY, DEFAULT_MACHINE_ID_TO_OPERATIONS_PER_SECOND, DEFAULT_JOB_SCHEDULER, DEFAULT_MAX_JOB_FAILURES);
	}
	
	public Grid(MachineStateFactory msf, Map<String, Long> machineIdToOperationsPerSecond, JobScheduler scheduler, long maxJobFailures) {
		
		this.msf = msf;
		this.machines = new LinkedList<Machine>();
		this.idleMachines = new HashSet<Machine>();
		this.waitingJobs = new LinkedList<Job>();
		this.allSubmittedJobs = new LinkedList<Job>();
		this.numMachinesNotBeingUsedByUser = 0;
		this.gridDemand = 0;
		this.machineIdToOperationsPerSecond = machineIdToOperationsPerSecond;
		this.scheduler = scheduler;
		this.maxJobFailures = maxJobFailures;
	}

	public boolean done() {
		
		if(!this.waitingJobs.isEmpty()) {
			return false;
		}
		
		boolean done = true;

		for (Iterator<Machine> iter = this.machines.iterator(); iter.hasNext() && done;) {
			done &= iter.next().done();
		}
		
		return done;
	}

	// XXX Currently, this is the only way we have to add machines into the grid
	public void setIdleMachine(String machineId, long idletime) {
		Machine m = null;
		for (Machine machine : this.machines) {
			if (machine.getId().equals(machineId)) {
				m = machine;
			}
		}

		if (m == null) {
			if(!machineIdToOperationsPerSecond.containsKey(machineId)){
				throw new IllegalArgumentException(String.format("'Operations per Second' for machine id '%s' not found!", machineId));
			}
			m = new Machine(this, machineId, this.msf.getStates(), machineIdToOperationsPerSecond.get(machineId));
			this.machines.add(m);
		}

		m.setIdle(idletime);
	}

	public void addJob(Job job) {
		addJobToPool(job);
		this.allSubmittedJobs.add(job);
	}

	private void addJobToPool(Job job) {
		this.waitingJobs.add(job);
	}

	public void update() {
		
		scheduleJobs();
		updateJobsMakespan();
		for (Machine m : this.machines) {
			m.update();
		}
		wakeUpMachinesIfNeeded();
		
	}

	public void canceledJob(Job job){
		job.incFailures();
		if(job.getFailures() < this.maxJobFailures){
			addJobToPool(job);
		}
	}
	
	private void updateJobsMakespan() {
		for (Job j : this.allSubmittedJobs) {
			if(!j.done()){
				j.incMakespan();
			}
		}
	}

	private void scheduleJobs() {
//		if (this.idleMachines.isEmpty()) return; //TODO Test this!!! If we don't have machines in idle states we still need to enter this method
		//because maybe we need to wake up machines!!!
		if (this.waitingJobs.isEmpty()) return;
		
		List<Pair<Machine, Job>> allocations = this.scheduler.schedule(new ArrayList<Machine>(idleMachines), this.waitingJobs);

		for (Pair<Machine, Job> allocation : allocations) {
			Machine m = Tuple.get1(allocation);
			Job j = Tuple.get2(allocation);
			
			this.idleMachines.remove(m);
			this.waitingJobs.remove(j);
			
			m.run(j);
		}
		
		wakeUpMachinesIfNeeded();
	}

	private void wakeUpMachinesIfNeeded() {

		long numMachinesToWakeUp = getNumMachinesToWakeUp();
		if(numMachinesToWakeUp != 0){
			wakeUpMachines(numMachinesToWakeUp);
		}
	}

	private long getNumMachinesToWakeUp() {
		
		if(this.waitingJobs.size() == 0) return 0;

		return Math.max(0, this.waitingJobs.size() - this.getMachines(MSEnum.LEAVING_GREEN).size() - this.getMachines(MSEnum.IDLE).size());
	}

	private void wakeUpMachines(long nmachines) {
		List<Machine> greenMachines = getMachines(MSEnum.GREEN);
		
		for (Machine m : greenMachines) {
			if(nmachines == 0) break;
			m.wakeUp();
			nmachines--;
		}
	}

	public int getNumMachinesNotBeingUsedByUser() {

		return this.numMachinesNotBeingUsedByUser;
	}

	public int getGridDemand() {

		return this.gridDemand + this.waitingJobs.size();
	}

	public List<Job> getJobs(){
		
		return this.allSubmittedJobs;
	}
	
	public List<Job> getWaitingJobs() {
		
		return this.waitingJobs;
	}

	public List<Machine> getMachines() {
		return machines;
	}
	
	public List<Machine> getMachines(MSEnum state) {
		
		List<Machine> machines = new LinkedList<Machine>();
		
		for (Machine m : this.machines) {
			if(m.isAtState(state)){
				machines.add(m);
			}
		}
		
		return machines;
		
	}
	
	public void setScheduler(JobScheduler scheduler) {
	
		this.scheduler = scheduler;
	}
	
	public void setMaxJobFailures(long maxJobFailures) {
		this.maxJobFailures = maxJobFailures;
	}
	
	public void setMsf(MachineStateFactory msf) {
	
		this.msf = msf;
	}

	public void setMachineIdToOperationsPerSecond(Map<String, Long> machineIdToOperationsPerSecond) {
	
		this.machineIdToOperationsPerSecond = machineIdToOperationsPerSecond;
	}

	public void machineChangedState(Machine m, MSEnum oldStateType, MSEnum newStateType) {

		if(newStateType.equals(MSEnum.IDLE)){
			this.idleMachines.add(m);
		} else if(newStateType.equals(MSEnum.USER)){
			this.numMachinesNotBeingUsedByUser--;
		} else if(newStateType.equals(MSEnum.GRID)){
			this.gridDemand++;
		}
		
		if(oldStateType.equals(MSEnum.IDLE)){
			this.idleMachines.remove(m);
		} else if(oldStateType.equals(MSEnum.USER)){
			this.numMachinesNotBeingUsedByUser++;
		} else if(oldStateType.equals(MSEnum.GRID)){
			this.gridDemand--;
		}
		
	}

}
